/*
 * Copyright (C) 2008 Peter Kling
 *
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 *
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * 'A Kind of Billiard'. If not, see <http://www.gnu.org/licenses/>.
 */

package cg2.billiard;

import java.awt.Font;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.Rectangle2D;

import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;

import cg2.glowifier.Glowifier;
import cg2.misc.JOGLWindowWidthMouseNavigation;
import cg2.misc.OpenGLDrawableGroup;
import cg2.misc.TranslatedOpenGLDrawable;
import cg2.models.GLModel;
import cg2.models.WavefrontObjLoader;
import cg2.particles.Particle;
import cg2.particles.SmokeParticleEngine;
import cg2.shadow.Shadower;

import com.sun.opengl.util.j2d.TextRenderer;

@SuppressWarnings("serial")
public class AKindOfBilliard extends JOGLWindowWidthMouseNavigation implements KeyListener {
	// some constants used for auto camera fly mode
	public static final double AUTOCAMERA_FREQUENCY = 0.1; 
	public static final double AUTOCAMERA_XRADIUS   = 6.0;
	public static final double AUTOCAMERA_YRADIUS   = 0.5;
	public static final double AUTOCAMERA_ZRADIUS   = 3.0;
	public static final double AUTOCAMERA_XOFFSET   = 0.0;
	public static final double AUTOCAMERA_YOFFSET   = 1.5;
	public static final double AUTOCAMERA_ZOFFSET   = 0.0;
	
	// some constants used for auto light mode
	public static final double AUTOLIGHT_FREQUENCY =  0.05; 
	public static final double AUTOLIGHT_XRADIUS   =  3.00;
	public static final double AUTOLIGHT_YRADIUS   =  0.50;
	public static final double AUTOLIGHT_ZRADIUS   =  2.50;
	public static final double AUTOLIGHT_XOFFSET   = -0.50;
	public static final double AUTOLIGHT_YOFFSET   =  1.00;
	public static final double AUTOLIGHT_ZOFFSET   =  0.00;
	
	// positional information for smoke engine/cigarette model
	public static final double[] smokePosition =
		{Billiard.DBU_POOLTABLE_8FEET_LENGTH/2. + 0.13, Billiard.DBU_POOLTABLE_8FEET_HEIGHT + 0.062, -0.15};
	
	// some models
	public static final String MODEL_FLOOR       = "models/floor.obj";
	public static final String MODEL_LIGHTSOURCE = "models/lightSource.obj";
	public static final String MODEL_ASHTRAY     = "models/ashtray.obj";
	private TranslatedOpenGLDrawable model_ashtray = null;
	private GLModel                  model_light   = null;
	
	// drawable group that holds the scene
	private OpenGLDrawableGroup scene;
	
	// variables for auto camera fly mode
	private double  x_camera_backup, rotx_camera_backup;
	private double  y_camera_backup, roty_camera_backup;
	private double  z_camera_backup, rotz_camera_backup;
	
	// shadower for scene
	private Shadower scene_shadowed = null;
	
	// glowifier for glowing objects
	private Glowifier billiardTable_glowified = null;
	private Glowifier cueIndicator_glowified  = null;
	private Glowifier light_glowified         = null;
	
	// billiard table
	private DBUPoolTable8Feet billiardTable = new DBUPoolTable8Feet();
	
	// configuration menu
	ConfigurationMenu cfgMenu = new ConfigurationMenu();
	
	// some configuration variables
	private boolean showConfigurationMenu       = false;
	private boolean drawCueIndicator            = false;
	private boolean drawCueIndicator_glowified  = false;
	private boolean drawBilliardTable_glowified = false;
	private boolean drawSceneShadowed           = false;
	private boolean drawLightSource             = false;
	private boolean drawShadowVolumes           = false;
	private boolean billiardTablePaused         = false;
	private boolean slowMachine                 = false;
	private boolean drawSmoke                   = false;
	private boolean autoCamera                  = false;
	private boolean autoLight                   = false;
	private int     currentLightIndex           = 0;
	
	// particle engine for smoke
	private SmokeParticleEngine smokeEngine = new SmokeParticleEngine(
			400, 2.25, 0.10, smokePosition[0], smokePosition[1], smokePosition[2], 2., 2., 0.5,
			new int[] {Particle.SINUS_CURVE, Particle.FIX_GRAVITY, Particle.SINUS_CURVE});
	
	// light0: directional, white, no ambient, specular
	private float[]   light0_ambient  = {0.0f, 0.0f, 0.0f, 0.0f};
	private float[]   light0_diffuse  = {0.8f, 0.8f, 0.8f, 1.0f};
	private float[]   light0_specular = {0.8f, 0.8f, 0.8f, 1.0f};
	private float     light0_spotExponent  = 0.f;
	private float     light0_spotCutoff    = 180.f;
	private float[][] light0_position = {
			{0.00f, 2.80f,  0.00f, 1.00f},
			{0.75f, 1.00f, -0.60f, 1.00f}};
	private float[][] light0_spotDirection = {
			{ 0.00f, -1.00f, 0.00f},
			{-0.25f, -0.25f, 1.00f}};
	private float[] lastLightPosition = new float[4];
	
	/**
	 * @brief Configuration menu for the billiard simulation.
	 * @TODO This should become a more general class. Furthermore, the code is simply ugly :-/.
	 */
	private class ConfigurationMenu {
		private final static int    TEXT_FONT_SIZE   = 30;
		private final static int    TEXT_BORDER_SIZE = 25;
		private TextRenderer textRenderer = null;
		
		/**
		 * @brief Standard constructor.
		 */
		public ConfigurationMenu() {
			textRenderer = new TextRenderer(new Font("SansSerif", Font.BOLD, TEXT_FONT_SIZE));
		}
		
		/**
		 * @brief Draw the configuration menu.
		 */
		public void draw(GLAutoDrawable drawable) {
			GL gl = drawable.getGL();
			
			// save several OpenGL state variables
			gl.glPushAttrib(GL.GL_COLOR_BUFFER_BIT | GL.GL_CURRENT_BIT | GL.GL_DEPTH_BUFFER_BIT | GL.GL_POLYGON_BIT |
					GL.GL_LIGHTING_BIT | GL.GL_TEXTURE_BIT);
			gl.glMatrixMode(GL.GL_PROJECTION);
			gl.glPushMatrix();
			gl.glMatrixMode(GL.GL_MODELVIEW);
			gl.glPushMatrix();
			
			// configure matrices
			gl.glMatrixMode(GL.GL_PROJECTION);
			gl.glLoadIdentity();
			gl.glOrtho(0., 1., 0., 1., -1., 1.);
			gl.glMatrixMode(GL.GL_MODELVIEW);
			gl.glLoadIdentity();
			
			// configuration of OpenGL state
			gl.glDisable(GL.GL_DEPTH_TEST);
			gl.glDisable(GL.GL_CULL_FACE);
			gl.glDisable(GL.GL_LIGHTING);
			gl.glDisable(GL.GL_TEXTURE_2D);
			gl.glEnable(GL.GL_BLEND);
			gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE);
			
			// draw overlay
			int numEntries = 17;
			double height = (numEntries*1.5*TEXT_FONT_SIZE + 2*TEXT_BORDER_SIZE) / drawable.getHeight();
			gl.glColor4d(0.75, 0.75, 0.75, 0.50);
			gl.glBegin(GL.GL_QUADS);
			gl.glVertex2d(0.1, 0.9-height);
			gl.glVertex2d(0.9, 0.9-height);
			gl.glVertex2d(0.9, 0.9);
			gl.glVertex2d(0.1, 0.9);
			gl.glEnd();
			
			// render menu text
			drawEntry(drawable, "Activate @Gravity",            billiardTable.getGravityEnabled(),  0);
			drawEntry(drawable, "Auto @camera",                 autoCamera,                         1);
			drawEntry(drawable, "Auto ligh@t",                  autoLight,                          2);
			drawEntry(drawable, "Draw cue @indicator",          drawCueIndicator,                   3);
			drawEntry(drawable, "Draw @light position",         currentLightIndex,                  4);
			drawEntry(drawable, "Draw light s@ource",           drawLightSource,                    5);
			drawEntry(drawable, "Draw shadow @volumes",         drawShadowVolumes,                  6);
			drawEntry(drawable, "Draw smoke (@N)",              drawSmoke,                          7);
			drawEntry(drawable, "Enforce cue stroke (@A)",                                          8);
			drawEntry(drawable, "Glow: billiard table (@1)",    drawBilliardTable_glowified,        9);
			drawEntry(drawable, "Glow: cue indicator (@2)",     drawCueIndicator_glowified,        10);
			drawEntry(drawable, "Lessen cue stroke (@Y)",                                          11);
			drawEntry(drawable, "@Pause (if gravity disabled)", billiardTablePaused,               12);
			drawEntry(drawable, "Reset cue @ball",                                                 13);
			drawEntry(drawable, "@Shadow/Lighting",             drawSceneShadowed,                 14);
			drawEntry(drawable, "Slow @machine",                slowMachine,                       15);
			drawEntry(drawable, "Show @help",                                                      16);
			
		    // restore saved OpenGL state
			gl.glPopMatrix();
			gl.glMatrixMode(GL.GL_PROJECTION);
			gl.glPopMatrix();
			gl.glMatrixMode(GL.GL_MODELVIEW);
		    gl.glPopAttrib();
		}
		
		/**
		 * @brief Draws the option with the given state at the given row position (starts at 0).
		 * 
		 * Note that the option name has to contain an @-character. The first such character will not be printed, but
		 * instead the following character will be printed in a different color to indicate a keyboard shortcut. Place
		 * the @-character at the end of the string if no shortcut indicator is desired.
		 */
		private void drawEntry(GLAutoDrawable drawable, String optName, boolean optState, int rowNumber) {
			textRenderer.beginRendering(drawable.getWidth(), drawable.getHeight());
		    drawEntryName(drawable, optName, rowNumber);
		    drawEntryValue(drawable, optState, rowNumber);
			textRenderer.endRendering();
		}
		
		/**
		 * @brief Draws the option with the given state at the given row position (starts at 0).
		 * 
		 * Note that the option name has to contain an @-character. The first such character will not be printed, but
		 * instead the following character will be printed in a different color to indicate a keyboard shortcut. Place
		 * the @-character at the end of the string if no shortcut indicator is desired.
		 */
		private void drawEntry(GLAutoDrawable drawable, String optName, int optValue, int rowNumber) {
			textRenderer.beginRendering(drawable.getWidth(), drawable.getHeight());
		    drawEntryName(drawable, optName, rowNumber);
		    drawEntryValue(drawable, optValue, rowNumber);
			textRenderer.endRendering();
		}
		
		/**
		 * @brief Draws an entry without any option value.
		 * 
		 * Note that the option name has to contain an @-character. The first such character will not be printed, but
		 * instead the following character will be printed in a different color to indicate a keyboard shortcut. Place
		 * the @-character at the end of the string if no shortcut indicator is desired.
		 */
		private void drawEntry(GLAutoDrawable drawable, String optName, int rowNumber) {
			textRenderer.beginRendering(drawable.getWidth(), drawable.getHeight());
		    drawEntryName(drawable, optName, rowNumber);
			textRenderer.endRendering();
		}
		
		/**
		 * @brief Draws the name of an option entry.
		 * 
		 * Call this method between TextRenderer.beginRendering() and TextRenderer.endRendering().
		 */
		private void drawEntryName(GLAutoDrawable drawable, String optName, int rowNumber) {
			int width  = drawable.getWidth();
			int height = drawable.getHeight();
			int top    = (int) Math.round(height*0.9) - TEXT_FONT_SIZE - TEXT_BORDER_SIZE;
			int left   = (int) Math.round(width*0.1)  + TEXT_BORDER_SIZE;
			float textColor_r, textColor_g, textColor_b, textColor_a;
			
			// render option name
			String subOptName;
			Rectangle2D optNameBounds;
			int shortcutSep = optName.indexOf('@');
		    textColor_r = 1.f; textColor_g = 1.f; textColor_b = 0.f; textColor_a = 0.7f;
			textRenderer.setColor(textColor_r, textColor_g, textColor_b, textColor_a);
			int textPos_x = left;
			int textPos_y = (int) Math.round(top - 1.5*rowNumber*TEXT_FONT_SIZE);
			subOptName = optName.substring(0, shortcutSep);
			textRenderer.draw(subOptName, textPos_x, textPos_y);
			if (shortcutSep < optName.length()-1) {
				optNameBounds = textRenderer.getBounds(subOptName);
				textPos_x += optNameBounds.getWidth();
				textColor_r = 0.f; textColor_g = 0.f; textColor_b = 1.f; textColor_a = 0.7f;
				textRenderer.setColor(textColor_r, textColor_g, textColor_b, textColor_a);
				subOptName = optName.substring(shortcutSep+1, shortcutSep+2);
				textRenderer.draw(subOptName, textPos_x, textPos_y);
				if (shortcutSep < optName.length()-2) {
					textPos_x += textRenderer.getBounds(optName.substring(shortcutSep+1, shortcutSep+2)).getWidth();
					textColor_r = 1.f; textColor_g = 1.f; textColor_b = 0.f; textColor_a = 0.7f;
					textRenderer.setColor(textColor_r, textColor_g, textColor_b, textColor_a);
					subOptName = optName.substring(shortcutSep+2);
					textRenderer.draw(subOptName, textPos_x, textPos_y);
				}
			}
		}
		
		/**
		 * @brief Draws the value of a boolean option entry.
		 * 
		 * Call this method between TextRenderer.beginRendering() and TextRenderer.endRendering().
		 */
		private void drawEntryValue(GLAutoDrawable drawable, boolean optState, int rowNumber) {
			int width  = drawable.getWidth();
			int height = drawable.getHeight();
			int top    = (int) Math.round(height*0.9) - TEXT_FONT_SIZE - TEXT_BORDER_SIZE;
			int right  = (int) Math.round(width*0.9)  - TEXT_BORDER_SIZE;
			float textColor_r, textColor_g, textColor_b, textColor_a;
			
			// render option value
			String optionStatus;
			if (optState == true) {
				optionStatus = "enabled";
				textColor_r = 0.f;
				textColor_g = 1.f;
				textColor_b = 0.f;
				 textColor_a = 0.7f;
			} else {
				optionStatus = "disabled";
				textColor_r = 1.f;
				textColor_g = 0.f;
				textColor_b = 0.f;
				 textColor_a = 0.35f;
			}
			Rectangle2D optStateEnabledBounds, optStateDisabledBounds;
			optStateEnabledBounds  = textRenderer.getBounds("enabled");
			optStateDisabledBounds = textRenderer.getBounds("disabled");
			double statusText_length = Math.max(optStateEnabledBounds.getWidth(), optStateDisabledBounds.getWidth());
			int textPos_x = (int) (right - statusText_length);
			int textPos_y = (int) Math.round(top - 1.5*rowNumber*TEXT_FONT_SIZE);
			textRenderer.setColor(textColor_r, textColor_g, textColor_b, textColor_a);
			textRenderer.draw(optionStatus, textPos_x, textPos_y);
		}
		
		/**
		 * @brief Draws the value of an integer option entry.
		 * 
		 * Call this method between TextRenderer.beginRendering() and TextRenderer.endRendering().
		 */
		private void drawEntryValue(GLAutoDrawable drawable, int optValue, int rowNumber) {
			int width  = drawable.getWidth();
			int height = drawable.getHeight();
			int top    = (int) Math.round(height*0.9) - TEXT_FONT_SIZE - TEXT_BORDER_SIZE;
			int right  = (int) Math.round(width*0.9)  - TEXT_BORDER_SIZE;
			float textColor_r, textColor_g, textColor_b, textColor_a;
			
			// render option value
			textColor_r = 0.f;
			textColor_g = 0.f;
			textColor_b = 0.f;
			textColor_a = 1.0f;
			String renderText = String.valueOf(optValue);
			Rectangle2D optValueBounds  = textRenderer.getBounds(renderText);
			double statusText_length = optValueBounds.getWidth();
			int textPos_x = (int) (right - statusText_length);
			int textPos_y = (int) Math.round(top - 1.5*rowNumber*TEXT_FONT_SIZE);
			textRenderer.setColor(textColor_r, textColor_g, textColor_b, textColor_a);
			textRenderer.draw(renderText, textPos_x, textPos_y);
		}
	}

	/**
	 * @brief Starts the billiard simulation.
	 */
	public static void main(String[] args) {
		AKindOfBilliard billiardGame = new AKindOfBilliard();
		billiardGame.setVisible(true);
	}
	
	public AKindOfBilliard() {
		// configure window
		setTitle("A Kind of Billiard");
		setSize(1000, 1000);
	}
	
	/**
	 * @brief Sets the camera position automatically as an ellipsoid function of the current time.
	 */
	private void applyAutoCameraTransformation(GL gl) {
		// get current time in seconds
		double time = System.currentTimeMillis()/1000.;
		
		// set current position of camera on ellipsoid around table
		double cameraX = AUTOCAMERA_XRADIUS*Math.cos(2*Math.PI*time*AUTOCAMERA_FREQUENCY) + AUTOCAMERA_XOFFSET;
		double cameraY = AUTOCAMERA_YRADIUS*Math.sin(2*Math.PI*time*AUTOCAMERA_FREQUENCY) + AUTOCAMERA_YOFFSET;
		double cameraZ = AUTOCAMERA_ZRADIUS*Math.sin(2*Math.PI*time*AUTOCAMERA_FREQUENCY) + AUTOCAMERA_ZOFFSET;
		
		// raise camera on the height of the table
		cameraY += billiardTable.getHeight();
		
		// let camera look to the table center
		glu.gluLookAt(cameraX, cameraY, cameraZ, 0, billiardTable.getHeight(), 0, 0, 1, 0);
	}
	
	/**
	 * @brief Sets the light position automatically as an ellipsoid function of the current time.
	 * 
	 * Additionally, this method will draw a small ball indicating the position of the light.
	 */
	private void applyAutoLightTransformation(GL gl) {
		// get current time in seconds
		double time = System.currentTimeMillis()/1000.;
		
		// set current light position on ellipsoid around table
		float[] lightPos = new float[3];
		lightPos[0] = (float) (AUTOLIGHT_XRADIUS*Math.cos(2*Math.PI*time*AUTOLIGHT_FREQUENCY) + AUTOLIGHT_XOFFSET);
		lightPos[1] = (float) (AUTOLIGHT_YRADIUS*Math.sin(2*Math.PI*time*AUTOLIGHT_FREQUENCY) + AUTOLIGHT_YOFFSET);
		lightPos[2] = (float) (AUTOLIGHT_ZRADIUS*Math.sin(2*Math.PI*time*AUTOLIGHT_FREQUENCY) + AUTOLIGHT_ZOFFSET);
		
		// raise light on the height of the table
		lightPos[1] += billiardTable.getHeight();
		
		// apply position/direction
		float[] lightDirection = new float[3];
		lightDirection[0] = -lightPos[0];
		lightDirection[1] = -lightPos[1];
		lightDirection[2] = -lightPos[2];
		gl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION      , lightPos, 0);
		gl.glLightfv(GL.GL_LIGHT0, GL.GL_SPOT_DIRECTION, lightDirection, 0);
		lastLightPosition = lightPos.clone();
	}
	
	/**
	 * @brief Main display method of the program.
	 */
	public void display(GLAutoDrawable drawable) {
		GL gl = drawable.getGL();
		
		// check whether we are in slow machine mode
		if (slowMachine == true)
			billiardTable.pause_time();
		
		// clear screen and save some OpenGL state variables
		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
		gl.glPushMatrix();
		
		// check whether to activate auto-camera
		if (autoCamera)
			applyAutoCameraTransformation(gl);
		else
			applyViewTransformation(gl);
		
		// set light position and direction
		if (!autoLight) {
			gl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION      , light0_position[currentLightIndex]     , 0);
			gl.glLightfv(GL.GL_LIGHT0, GL.GL_SPOT_DIRECTION, light0_spotDirection[currentLightIndex], 0);
			lastLightPosition = light0_position[currentLightIndex].clone();
		} else {
			applyAutoLightTransformation(gl);
		}
		
		// draw billiard table
		if (drawSceneShadowed) {
			// paused to avoid shadowing of balls while they are moving due to volume shadowing
			billiardTable.pause();
			scene_shadowed.draw(gl);
			billiardTable.resume();
		} else {
			scene.draw(gl);
		}
		
		// draw shadow volumes (if requested)
		if (drawShadowVolumes)
			scene_shadowed.draw_shadowVolume(gl);
		
		// draw glowing billiard table
		if (drawBilliardTable_glowified == true)
			billiardTable_glowified.draw(gl);
		
		// draw cue indicator
		if (drawCueIndicator == true) {
			billiardTable.getCueIndicator().draw(gl);
			if (drawCueIndicator_glowified == true)
				cueIndicator_glowified.draw(gl);
		}
		
		// draw ashtray
		model_ashtray.draw(gl);
		
		// draw smoke
		if (drawSmoke)
			smokeEngine.updateParticles(gl);
		
		// draw light source
		if (drawLightSource) {
			gl.glDisable(GL.GL_TEXTURE_2D);
			gl.glDisable(GL.GL_LIGHTING);
			gl.glColor4d(1., 1., 0., 1.);
			gl.glPushMatrix();
			gl.glTranslatef(lastLightPosition[0], lastLightPosition[1], lastLightPosition[2]);
			model_light.draw(gl);
			light_glowified.draw(gl);
			gl.glPopMatrix();
			gl.glEnable(GL.GL_LIGHTING);
		}
		
		// draw configuration menu
		if (showConfigurationMenu)
			cfgMenu.draw(drawable);
	    
		// restore save OpenGL state variables
		gl.glPopMatrix();
		
		// check whether we are in slow machine mode
		if (slowMachine == true)
			billiardTable.resume();
	}
	
	/**
	 * @brief Initializes the OpenGL context.
	 */
	@Override
	public void init(GLAutoDrawable drawable) {
		super.init(drawable);
		GL gl = drawable.getGL();
		
		// register key listener
		drawable.addKeyListener(this);
		
		// configuration of OpenGL
		gl.glEnable(GL.GL_DEPTH_TEST);
		gl.glEnable(GL.GL_CULL_FACE);
		gl.glCullFace(GL.GL_BACK);
		
		// configure and enable light0
		gl.glEnable(GL.GL_LIGHTING);
		gl.glEnable(GL.GL_LIGHT0);
		gl.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT , light0_ambient , 0);
		gl.glLightfv(GL.GL_LIGHT0, GL.GL_DIFFUSE , light0_diffuse , 0);
		gl.glLightfv(GL.GL_LIGHT0, GL.GL_SPECULAR, light0_specular, 0);
		gl.glLightf(GL.GL_LIGHT0, GL.GL_SPOT_EXPONENT, light0_spotExponent);
		gl.glLightf(GL.GL_LIGHT0, GL.GL_SPOT_CUTOFF  , light0_spotCutoff);
		
		// configure OpenGL light model
		float[] lightmodel_ambient = {0.5f, 0.5f, 0.5f, 1.0f};
		gl.glLightModelfv(GL.GL_LIGHT_MODEL_AMBIENT, lightmodel_ambient, 0);

		// initialize billiard table
		billiardTable.init(gl);
		
		// initialize smoke engine
		smokeEngine.init(gl);
		
		// load some models
		model_ashtray = new TranslatedOpenGLDrawable(WavefrontObjLoader.loadObject(gl, MODEL_ASHTRAY), smokePosition[0],
				smokePosition[1], smokePosition[2]);
		model_light = WavefrontObjLoader.loadObject(gl, MODEL_LIGHTSOURCE);
		
		// create scene group
		scene = new OpenGLDrawableGroup();
		scene.add(billiardTable);
		scene.add(WavefrontObjLoader.loadObject(gl, MODEL_FLOOR));
		
		// setup shadower for scene
		scene_shadowed = new Shadower(scene);
		scene_shadowed.init(gl);
		
		// setup glowifiers
		billiardTable_glowified = new Glowifier(billiardTable, 3., 4.5, 0.3);
		billiardTable_glowified.init(gl, 100, 100);
		cueIndicator_glowified = new Glowifier(billiardTable.getCueIndicator(), 3., 4.5, 0.4);
		cueIndicator_glowified.init(gl, 100, 100);
		light_glowified = new Glowifier(model_light, 6., 6., 0.5);
		light_glowified.init(gl, 500, 500);
	}

	/**
	 * @brief Keyboard handling.
	 */
	public void keyPressed(KeyEvent e) {
		// control next cue stroke
		if (e.getKeyCode() == KeyEvent.VK_A)
			billiardTable.increaseCueVelocity();
		if (e.getKeyCode() == KeyEvent.VK_Y)
			billiardTable.decreaseCueVelocity();
		if (e.getKeyCode() == KeyEvent.VK_UP)
			billiardTable.rotateCue_up();
		if (e.getKeyCode() == KeyEvent.VK_DOWN)
			billiardTable.rotateCue_down();
		if (e.getKeyCode() == KeyEvent.VK_LEFT)
			billiardTable.rotateCue_left();
		if (e.getKeyCode() == KeyEvent.VK_RIGHT)
			billiardTable.rotateCue_right();
		
		// stroke with the cue
		if (e.getKeyCode() == KeyEvent.VK_SPACE)
			billiardTable.cueStroke();
		
		// enable/disable automatic camera movement
		if (e.getKeyCode() == KeyEvent.VK_C) {
			autoCamera = !autoCamera;
			
			// backup/restore original camera position/orientation
			if (autoCamera == false) {
				x_camera = x_camera_backup;
				y_camera = y_camera_backup;
				z_camera = z_camera_backup;
				rotx_camera = rotx_camera_backup;
				roty_camera = roty_camera_backup;
				rotz_camera = rotz_camera_backup;
			} else {
				x_camera_backup = x_camera;
				y_camera_backup = y_camera;
				z_camera_backup = z_camera;
				rotx_camera_backup = rotx_camera;
				roty_camera_backup = roty_camera;
				rotz_camera_backup = rotz_camera;
			}
		}
		
		// enable/disable automatic light movement
		if (e.getKeyCode() == KeyEvent.VK_T)
			autoLight = !autoLight;
		
		// reset ball positions
		if (e.getKeyCode() == KeyEvent.VK_R)
			billiardTable.reset();
		
		// reset cue ball 
		if (e.getKeyCode() == KeyEvent.VK_B)
			billiardTable.resetCueBall();
		
		// enable/disable cueIndicator
		if (e.getKeyCode() == KeyEvent.VK_I)
			drawCueIndicator = !drawCueIndicator;
		
		// enable/disable shadowing
		if (e.getKeyCode() == KeyEvent.VK_S)
			drawSceneShadowed = !drawSceneShadowed;
		
		// enable/disable drawing of shadow volumes
		if (e.getKeyCode() == KeyEvent.VK_V)
			drawShadowVolumes = !drawShadowVolumes;
		
		// iterate over the different light positions
		if (e.getKeyCode() == KeyEvent.VK_L)
			currentLightIndex = (currentLightIndex + 1) % light0_position.length;
		
		// enable/disable drawing of light source
		if (e.getKeyCode() == KeyEvent.VK_O)
			drawLightSource = !drawLightSource;
		
		// enable/disable glow effects
		if (e.getKeyCode() == KeyEvent.VK_1)
			drawBilliardTable_glowified = !drawBilliardTable_glowified;
		if (e.getKeyCode() == KeyEvent.VK_2)
			drawCueIndicator_glowified = !drawCueIndicator_glowified;
		
		// enable/disable smoke engine
		if (e.getKeyCode() == KeyEvent.VK_N)
			drawSmoke = !drawSmoke;
		
		// enable/disable gravity for balls
		if (e.getKeyCode() == KeyEvent.VK_G)
			billiardTable.toggleGravity();
		
		// enable slow-machine mode (time will be paused for balls during rendering)
		if (e.getKeyCode() == KeyEvent.VK_M)
			slowMachine = !slowMachine;
		
		// pauses/unpauses the table
		if (e.getKeyCode() == KeyEvent.VK_P) {
			billiardTablePaused = !billiardTablePaused;
			if (billiardTablePaused)
				billiardTable.pause_time();
			else
				billiardTable.resume();
		}
		
		// toggle configuration menu
		if (e.getKeyCode() == KeyEvent.VK_H)
			showConfigurationMenu = !showConfigurationMenu;
	}

	public void keyReleased(KeyEvent e) { /* void */ }

	public void keyTyped(KeyEvent e) { /* void */ }
}
